/*
 * BTServerNetworkManager.java
 *    Author: nfk
 *      Date: Sep 19, 2008
 *      
 *   Classes:
 *      BTServerNetworkManager
 */

package at.syncme.framework.midp;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.Vector;

import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

import at.syncme.framework.Event;

/**
 * A Bluetooth network manager server.
 * 
 * @author nfk
 * @author patchinsky
 */
public final class BTServerNetworkManager implements ServerNetworkManager,
        Runnable {

    static {
        try {
            LocalDevice.getLocalDevice().setDiscoverable(DiscoveryAgent.GIAC);
            supportsBluetooth = true;
        } catch (final BluetoothStateException ex) {
            supportsBluetooth = false;
        }
    }

    private static boolean supportsBluetooth;

    private static BTServerNetworkManager nm;

    private String uuid;

    private String name;

    private String listenURL;

    private boolean isListening;

    private StreamConnectionNotifier listener;

    private Vector connections;

    private Vector outputStreams;

    private Sender sender;

    /**
     * Returns the network manager instance.
     * 
     * @param name
     * @param uuid
     * @return the network manager instance.
     */
    public static BTServerNetworkManager getManager(String name, String uuid) {
        if (nm == null) {
            nm = new BTServerNetworkManager(name, uuid);
        }
        return nm;
    }

    /**
     * Forbid construction.
     * 
     * @param uuid
     * @param name
     */
    private BTServerNetworkManager(String name, String uuid) {
        this.name = name;
        this.uuid = uuid;
        this.listenURL = "btspp://localhost:" + this.uuid + ";name="
                + this.name;
        this.connections = new Vector();
        this.outputStreams = new Vector();
    }

    /**
     * @see at.syncme.framework.midp.ServerNetworkManager#isListening()
     */
    public boolean isListening() {
        return this.isListening;
    }

    /**
     * @see at.syncme.framework.midp.ServerNetworkManager#listen()
     */
    public void listen() throws IOException {
        if (isListening()) {
            throw new IllegalStateException("Already listening.");
        }

        new Thread(this).start();
    }

    /**
     * @see at.syncme.framework.midp.NetworkManager#disconnect()
     */
    public void disconnect() {
        OutputStream os = null;
        for (int i = 0; i < this.outputStreams.size(); i++) {
            os = (OutputStream) this.outputStreams.elementAt(i);

            try {
                os.close();
            } catch (final IOException ex) {
                // Ignore
            }

            this.outputStreams.removeElement(os);
        }

        StreamConnection sc = null;
        for (int i = 0; i < this.connections.size(); i++) {
            sc = (StreamConnection) this.connections.elementAt(i);

            try {
                sc.close();
            } catch (final IOException ex) {
                // Ignore
            }

            this.connections.removeElement(sc);
        }

        stop();
    }

    /**
     * @see at.syncme.framework.midp.NetworkManager#receive()
     */
    public Event receive() throws IOException {
        throw new UnsupportedOperationException();
    }

    /**
     * @see at.syncme.framework.midp.NetworkManager#send(at.syncme.framework.Event)
     */
    public void send(final Event e) throws IOException {
        if (!isListening() || this.sender == null) {
            throw new IllegalStateException(
                    "Not listening || no sender active.");
        }

        this.sender.setSendData(e);
    }

    /**
     * @see at.syncme.framework.midp.ServerNetworkManager#sendUnicast(short,
     *      at.syncme.framework.Event)
     */
    public void sendUnicast(short nodeId, Event e) throws IOException {
        synchronized (this.outputStreams) {
            DataOutputStream out = (DataOutputStream) this.outputStreams
                    .elementAt(nodeId);
            if (out != null) { // may be dead in the meanwhile
                e.serialize(out);
                out.flush();
            }
        }
    }

    /**
     * @see java.lang.Runnable#run()
     */
    public void run() {
        try {
            this.listener = (StreamConnectionNotifier) Connector
                    .open(listenURL);
            this.isListening = true;
            StreamConnection connection = null;

            this.sender = new Sender();
            this.sender.start();

            while (this.isListening) {
                connection = this.listener.acceptAndOpen();
                DataOutputStream out = new DataOutputStream(connection
                        .openOutputStream());
                this.connections.addElement(connection);
                this.outputStreams.addElement(out);
                new Receiver(connection, out,
                        (short) (outputStreams.size() - 1)).start();
            }
        } catch (final InterruptedIOException ex) {
            // normal shutdown after disconnect
            stop();
        } catch (final IOException ex) {
            stop();
        } finally {
            stop();
        }
    }

    /**
     * @see at.syncme.framework.midp.ServerNetworkManager#stop()
     */
    public void stop() {
        if (isListening()) {
            this.isListening = false;

            try {
                this.listener.close();
            } catch (final IOException ex) {
                // Ignore
            }
        }
    }

    /**
     * A simple class to for threaded receival.
     * 
     * @author nfk
     */
    private final class Receiver extends Thread {

        private final StreamConnection connection;

        private final DataInputStream is;

        private final DataOutputStream os;

        private final short nodeId;

        /**
         * Constructs a new instance of Receiver.
         * 
         * @param sc
         *            the stream connection to handle
         * @param os
         *            the outputstream to remove when something fails
         * @param nodeId
         *            the assigned nodeId
         * @throws NullPointerException
         *             if sc is null
         * @throws IllegalStateException
         *             if no event listener is set
         */
        public Receiver(final StreamConnection sc, DataOutputStream os,
                short nodeId) {
            if (sc == null) {
                throw new NullPointerException("sc must not be null.");
            }

            try {
                this.is = new DataInputStream(sc.openInputStream());
                this.os = os;
                this.connection = sc;
                this.nodeId = nodeId;
            } catch (final IOException ex) {
                throw new IllegalStateException("Opening input stream failed.");
            }
        }

        /**
         * @see java.lang.Thread#run()
         */
        public void run() {
            Event e = null;

            while (isListening) {
                try {
                    e = Event.materialize(this.is, this.nodeId);
                } catch (final IOException ex) {
                    break;
                }
            }

            // produce some garbage now
            cleanup(this.nodeId);
        }

        /**
         * cleanup everything
         * 
         * @param nodeId
         */
        private void cleanup(short node) {
            try {
                // now cleanup
                this.is.close(); // should be already away

                // nothing comes in, why should go something out?
                synchronized (outputStreams) {
                    outputStreams.removeElementAt(node);
                }
                // there is actually no connection
                synchronized (connections) {
                    connections.removeElement(connection);
                }
            } catch (IOException ex) {
                // Ignore
            }
        }
    }

    /**
     * A thread class to broadcast strings to a handful of output streams.
     * 
     * @author nfk
     */
    private final class Sender extends Thread {
        private Vector queue = new Vector();

        private int start = 0;

        /**
         * @see java.lang.Thread#run()
         */
        public void run() {
            Event[] queueClone;
            DataOutputStream[] streamsClone;

            while (isListening) {
                synchronized (this) {
                    while (this.queue.isEmpty()) {
                        try {
                            this.wait();
                        } catch (final InterruptedException ex) {
                            // Ignore
                        }
                    }

                    queueClone = new Event[this.queue.size()];
                    this.queue.copyInto(queueClone);
                    this.queue.removeAllElements();

                    streamsClone = new DataOutputStream[outputStreams.size()];
                    outputStreams.copyInto(streamsClone);
                }

                if (this.start >= streamsClone.length) {
                    this.start = 0;
                }

                // Send everything in buffer in fair turn-around of the first
                // output stream
                DataOutputStream os = null;
                Event e = null;
                for (int i = 0; i < queueClone.length; i++) {
                    e = queueClone[i];

                    for (int m = this.start; m < streamsClone.length; m++) {
                        os = streamsClone[m];
                        sendHelper(os, e);
                    }

                    if (this.start != 0) {
                        for (int n = 0; start < streamsClone.length
                                && n < this.start; n++) {
                            os = streamsClone[n];
                            sendHelper(os, e);
                        }
                    }
                    if ((this.start + 1) >= streamsClone.length) {
                        start = 0;
                    } else {
                        this.start++;
                    }
                }
            }
        }

        /** Helper method to send to output streams and handle events. */
        private synchronized void sendHelper(final DataOutputStream os,
                final Event e) {
            try {
                e.serialize(os);
                os.flush();
            } catch (final IOException ex) {
                if (outputStreams.contains(os)) {
                    outputStreams.removeElement(os);
                }
            }
        }

        /** Helper method to set data from the outside. */
        private synchronized void setSendData(final Event e) {
            if (e == null) {
                throw new NullPointerException("e must not be null.");
            }
            if (this.queue == null) {
                throw new NullPointerException("queue is null");
            }

            this.queue.addElement(e);
            this.notifyAll();
        }

    }

    /**
     * Returns if the device supports bluetooth resp. if bluetooth is turned on.
     * 
     * @return
     */
    public static boolean supportsBluetooth() {
        return supportsBluetooth;
    }

}

/* End of BTServerNetworkManager.java */
